/*
 * Copyright (c) 2017 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */

class SmartCoffeeApp {

    constructor(config, connectionConfigFunction) {
        this.config = config;
        this.features = this.config.thingJson.features;
        this.connectionConfigFunction = connectionConfigFunction;
        this.initUI();
        this.initWebSocket();
    }

    initUI() {
        this.firstConnection = true;
        this.uiSmartCoffeeId = "#smartCoffee";
        this.registerUICallbacks();
    }

    initWebSocket() {
        const pingInterval = 60000;
        this.thing = new DittoWebSocket(pingInterval);
        this.thing.setOnMessage((message) => this.onMessage(message));
        this.thing.setOnClosed(() => this.onClosed());
    }


    registerForMessages() {
        this.thing.sendRaw('START-SEND-MESSAGES');
        this.logSendToUI(undefined, 'START-SEND-MESSAGES', '', 'tell Ditto that i want to receive Messages');
    }

    /* ***************************************************************** */
    /* ********************* UI callbacks ****************************** */
    /* ***************************************************************** */

    registerUICallbacks() {
        $('#connect').click(() => this.connect(this.connectionConfigFunction()));
        $(`${this.uiSmartCoffeeId} .dismiss-all`).click(() => this.dismissAllUILogs());
    }

    connect(connectionConfig) {
        if (!this.firstConnection) {
            this.thing.close();
        }

        this.markAsConnecting();
        this.thing.connect(connectionConfig, () => this.onConnected());
    }

    dismissAllUILogs() {
        $(`${this.uiSmartCoffeeId} .alert>button`).click();
    }

    /* ***************************************************************** */
    /* ******************* WebSocket callbacks ************************* */
    /* ***************************************************************** */

    onConnected() {
        this.logToConsole('connected');
        this.registerForMessages();
    }

    onClosed() {
        this.markAsClosed();
        this.logToConsole('connection closed.');
    }

    onMessage(message) {
        const data = message.data;
        this.logToConsole(`received: ${data}`);
        this.logReceiveToUI(data);

        if (this.isStartSendMessagesAck(data)) {
            this.onRegisteredForMessages();
        } else {
            try {
                const jsonMessage = JSON.parse(message.data);
                this.onJsonMessage(jsonMessage);
            } catch (err) {
                this.logToConsole(`error while handling message: ${err}`);
            }
        }
    }

    /* ***************************************************************** */
    /* ******************* Message handling **************************** */
    /* ***************************************************************** */

    onJsonMessage(jsonMessage) {
        if (this.isMakeCoffeeMessage(jsonMessage)) {
            this.onMakeCoffeeMessage(jsonMessage);
        } else if (this.isTurnOnWaterTankMessage(jsonMessage)) {
            this.onTurnOnWaterTankMessage(jsonMessage);
        } else if (this.isTurnOffWaterTankMessage(jsonMessage)) {
            this.onTurnOffWaterTankMessage(jsonMessage);
        } else {
            this.logToConsole(`unhandled topic: ${jsonMessage.topic}`);
        }
    }

    isMakeCoffeeMessage(jsonMessage) {
        const thingIdAsPath = this.toPath(this.config.thingId);
        const makeCoffeeSubject = this.config.coffeeMachine.makeCoffeeSubject;
        const makeCoffeeTopic = `${thingIdAsPath}/things/live/messages/${makeCoffeeSubject}`;

        return makeCoffeeTopic === jsonMessage.topic;
    }

    isTurnOnWaterTankMessage(jsonMessage) {
        const thingIdAsPath = this.toPath(this.config.thingId);
        const turnOnWaterTankSubject = this.config.waterTank.onSubject;
        const waterTankFeature = this.config.waterTank.feature;
        const turnOnWaterTankTopic = `${thingIdAsPath}/things/live/messages/${turnOnWaterTankSubject}`;
        const turnOnWaterPath = `/features/${waterTankFeature}/inbox/messages/${turnOnWaterTankSubject}`;

        return turnOnWaterTankTopic === jsonMessage.topic &&
            turnOnWaterPath === jsonMessage.path;
    }

    isTurnOffWaterTankMessage(jsonMessage) {
        const thingIdAsPath = this.toPath(this.config.thingId);
        const turnOffWaterTankSubject = this.config.waterTank.offSubject;
        const waterTankFeature = this.config.waterTank.feature;

        const turnOffWaterTankTopic = `${thingIdAsPath}/things/live/messages/${turnOffWaterTankSubject}`;
        const turnOffWaterPath = `/features/${waterTankFeature}/inbox/messages/${turnOffWaterTankSubject}`;

        return turnOffWaterTankTopic === jsonMessage.topic &&
            turnOffWaterPath === jsonMessage.path;
    }

    onMakeCoffeeMessage(jsonMessage) {
        if (this.isMakeCoffeeCaptchaCorrect(jsonMessage)) {
            // captcha was correctly solved -> brew the coffee
            this.brewCoffee();

            const successResponse = JSON.stringify({
                'captchaSolved': true
            });
            this.logSendToUI(successResponse, 200, jsonMessage.topic, 'inform that coffee is brewed');
            this.thing.reply(jsonMessage,
                successResponse,
                "application/json",
                200);
        } else {
            // captcha is incorrect -> reply with a new captcha image
            const captcha = this.createCaptchaMessage(b64Captcha());
            this.logSendToUI(this.createCaptchaMessage('[b64-encoded-captcha-image]'), 400, jsonMessage.topic, 'tell requester to solve captcha to be able to brew a coffee');
            this.thing.reply(jsonMessage,
                captcha,
                'image/png',
                400);
        }
    }

    isMakeCoffeeCaptchaCorrect(jsonMessage) {
        return 'ditto' === jsonMessage.value.captcha;
    }

    brewCoffee() {
        // update ui
        this.markCoffeeAsBrewing();

        // update brewing counter
        this.features['coffee-brewer'].properties['brewed-coffees']++;

        // update brewing counter of twin
        const thingIdAsPath = this.toPath(this.config.thingId);
        const updateFeatureMessage = this.thing.protocolEnvelope(
            `${thingIdAsPath}/things/twin/commands/modify`,
            'features/coffee-brewer/properties/brewed-coffees',
            this.features['coffee-brewer'].properties['brewed-coffees']
        );
        this.logSendToUI(JSON.stringify(updateFeatureMessage), '', updateFeatureMessage.topic, 'update the brewing counter of my twin representation');
        this.thing.send(updateFeatureMessage);
    }

    createCaptchaMessage(imageToSolve) {
        return imageToSolve;
    }

    onTurnOnWaterTankMessage(jsonMessage) {
        this.thing.reply(jsonMessage, this.config.waterTank.successResponse, "application/json", 200);
        this.logSendToUI(this.config.waterTank.successResponse, 200, jsonMessage.topic, 'tell requester that water tank was successfully turned on');
        this.markWaterAsActive();
    }

    onTurnOffWaterTankMessage(jsonMessage) {
        this.thing.reply(jsonMessage, this.config.waterTank.successResponse, "application/json", 200);
        this.logSendToUI(this.config.waterTank.successResponse, 200, jsonMessage.topic, 'tell requester that water tank was successfully turned off');
        this.markWaterAsInactive();
    }

    onRegisteredForMessages() {
        this.logToConsole('registered for messages');
        this.markAsConnected();
        if (this.firstConnection) {
            this.renameConnectButton('Reconnect');
            this.firstConnection = false;
        }
    }

    toPath(thingId) {
        return thingId.replace(':', '/');
    }

    isStartSendMessagesAck(message) {
        return message === 'START-SEND-MESSAGES:ACK';
    }

    /* ********************************************************************** */
    /* ******************               UI            *********************** */
    /* ********************************************************************** */

    logToConsole(message) {
        console.log(`[${this.config.thingId}] ${message}`);
    }

    renameConnectButton(newText) {
        $('#connect').html(newText);
    }

    markCoffeeAsBrewing() {
        $("#coffee").addClass("active");

        if (isDefined(this.timeoutId)) {
            clearTimeout(this.timeoutId);
            this.timeoutId = undefined;
        }
        // stop brewing after some seconds
        const newTimeoutId = setTimeout(
            () => {
                $("#coffee").removeClass("active");
                clearTimeout(this.timeoutId);
            },
            5000
        );
        this.timeoutId = newTimeoutId;
    }

    markWaterAsActive() {
        $("#water").addClass("active");
    }

    markWaterAsInactive() {
        $("#water").removeClass("active");
    }

    markAsConnected() {
        $("#wsinfo>.closed").hide();
        $("#wsinfo>.opening").hide();
        $("#wsinfo>.opened").show();
    }

    markAsClosed() {
        $("#wsinfo>.closed").show();
        $("#wsinfo>.opening").hide();
        $("#wsinfo>.opened").hide();
    }

    markAsConnecting() {
        $("#wsinfo>.closed").hide();
        $("#wsinfo>.opening").show();
        $("#wsinfo>.opened").hide();
    }

    logSendToUI(data, statusCode, topic, doc) {
        const htmlCmd = `<h4>⇦ ${isDefined(statusCode) ? statusCode : ''}</h4>
                      <p class="break-word">${isDefined(data) ? data : ''}</p>
                      <hr>
                      <div><small class="break-word">${topic}</small></div>
                      ${isDefined(doc) ? `<div><small class="break-word text-muted">${doc}</small></div>`: ''}`;
        this.logToUi('info', htmlCmd);
    }

    logReceiveToUI(data) {
        const htmlReceive = `<h4>⇨</h4>
                          <div class="break-word">${data}</div>`;
        this.logToUi('success', htmlReceive);
    }

    logToUi(role, message) {
        $("#sc-alerts").append(
            `<div class="alert alert-${role} alert-dismissible fade show" role="alert">
                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                    </button>
                    ${message}
             </div>`);
    }

}


class DittoWebSocket {

    constructor(pingInterval = 60000) {
        this.pingInterval = pingInterval;
        this.stopPing = false;
        this.clientCallbacks = {};
    }

    connect(connectionConfig, callback) {
        const baseUrl = `ws://${connectionConfig.getUsername()}:${connectionConfig.getPassword()}@${connectionConfig.getHost()}/ws/1`;
        this.ws = new WebSocket(baseUrl);
        this.ws.onopen = () => this.onOpen(callback);
    }

    close(callback) {
        this.ws.onclose = callback;
        this.ws.close();
    }

    onOpen(callback) {
        // define as functions, so the message is executed in current context
        this.ws.onmessage = (message) => this.onMessage(message);
        this.ws.onclose = () => this.onClosed();
        this.ws.onerror = (error) => this.onError(error);
        if (isDefined(callback)) {
            callback();
        }
        this.schedulePingMessage();
    }

    onMessage(message) {
        if (isDefined(this.clientCallbacks.onMessage)) {
            this.clientCallbacks.onMessage(message);
        }
    }

    onClosed() {
        this.stopPing = true;
        if (isDefined(this.clientCallbacks.onClosed)) {
            this.clientCallbacks.onClosed();
        }
    }

    onError(error) {
        console.log(`error: ${e}`);
        if (isDefined(this.clientCallbacks.onError)) {
            this.clientCallbacks.onError(error);
        }
    }


    sendRaw(content) {
        this.ws.send(content);
    }

    send(json) {
        console.log(`sending JSON ${JSON.stringify(json)}`);
        this.sendRaw(JSON.stringify(json));
    }

    reply(message, payload, contentType, status) {
        const response = Object.assign({
            status
        }, this.protocolMessage(
            message.headers['thing-id'],
            message.topic,
            message.headers.subject,
            message.headers['correlation-id'],
            contentType,
            "FROM",
            message.path.replace("inbox", "outbox"),
            payload
        ));

        this.send(response);
    }

    /**
     * Create a Ditto protocol WebSocket API Message.
     */
    protocolMessage(thingId, topic, subject, correlationId, contentType, direction, path, payload) {
        return Object.assign({
                "headers": {
                    "thing-id": thingId,
                    subject,
                    "correlation-id": correlationId,
                    "content-type": contentType,
                    direction
                }
            },
            this.protocolEnvelope(topic, path, payload)
        );
    }

    /*
     * Create a ditto protocol envelope.
     */
    protocolEnvelope(topic, path, value) {
        return {
            topic,
            path,
            value
        };
    }

    setOnMessage(onMessage) {
        this.clientCallbacks.onMessage = onMessage;
    }

    setOnClosed(onClosed) {
        this.clientCallbacks.onClosed = onClosed;
    }

    setOnError(onError) {
        this.clientCallbacks.onError = onError;
    }

    schedulePingMessage() {
        setTimeout(() => this.sendPingMessage(), this.pingInterval);
    }

    sendPingMessage() {
        if (this.stopPing) {
            this.stopPing = false;
        } else {
            this.sendRaw(new ArrayBuffer(0));
            this.schedulePingMessage();
        }
    }
}

isDefined = (arg) => typeof arg !== 'undefined';

b64Captcha = () => '';
